fix(engine): report the cause and attempt count when a node exhausts retries - #77
Conversation
…retries
`node failed after retries` was emitted with the node id and nothing else, so
a report of it could not be root-caused: the log said a node failed but not
why, and not how many attempts it took.
`last_err` is already a parameter of `finish_execution` and is in scope at the
warning — it is simply not unwrapped until 44 lines further down. Name it on
the line (borrowed; the unwrap below still takes it by value), along with the
`on_error` policy that decides what happens next, and thread `attempts_used`
through from the retry loop so "after retries" carries a number.
Logged at the existing site rather than after the `let Some(err)` unwrap: the
`None` arm there is the defensive unreachable path, and moving the line past
it would stop that case reporting at all.
Log-only; no control flow changes. The failure was already routed
independently via the `ExecutionStep { status: Error }` pushed just below.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe execution finalization path now receives the executor attempt count and emits structured retries-exhausted warnings. A mock-feature end-to-end test captures tracing events and verifies the node, error, attempt count, and error policy. ChangesRetries-exhausted diagnostics
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The change only adds retry count, policy, and failure details to exhausted-node warnings without changing execution behavior; merge is reasonable with explicit owner awareness that error payload fragments may now appear in production logs. Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Warning Your free Security trial is over. An organization admin can activate billing to continue. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
How this change flows1 changed behaviour across 12 relationships. 5 surrounding behaviours are shown (60 graph nodes walked). 39 further behaviours left out to keep the diagram readable. flowchart LR
n0["HandlerData<br/>changed"]:::changed
n1["finish_execution"]:::impacted
n2["execute"]:::impacted
n3["Send"]:::impacted
n4["Node"]:::impacted
n5["RunObserver"]:::impacted
n0 -->|uses| n4
n0 -->|uses| n5
n1 -->|calls| n3
n1 -->|uses| n3
n1 -->|uses| n4
n1 -->|uses| n5
n2 -->|calls| n1
n2 -->|calls| n3
n2 -->|uses| n3
n2 -->|calls| n4
n5 -->|uses| n3
n5 -->|implements| n3
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge. |
Summary
node failed after retrieswas emitted with the node id and nothing else:That is the only line a run produces when a node gives up, so a report of it cannot be root-caused — the log says a node failed, but not why, and not how many attempts it took. This is exactly what happened to
tinyhumansai/openhuman#5626, which is a single staging occurrence of this warning and cannot be diagnosed as filed.The cause was already in hand and simply not named.
last_err: Option<EngineError>is a parameter offinish_executionand is in scope at the warning; it is just not unwrapped until thelet Some(err)44 lines further down. This borrows it there (the unwrap below still takes it by value), adds theon_errorpolicy that decides what happens next, and threadsattempts_usedthrough from the retry loop so "after retries" carries a number.Deliberately logged at the existing site rather than after the unwrap: the
Nonearm below it is the defensive unreachable path, and moving the line past it would stop that case reporting at all.What this is not
This is a diagnostic prerequisite, not a root-cause fix. It does not explain why any particular node failed, and it is not expected to change any observed behaviour.
tinyhumansai/openhuman#5626needs the workflow graph containing the node and a staging log window to go further, neither of which exists in a repository — that issue should stay open, and closing it needs a person, not this PR.Worth stating for anyone reading the linked issue:
[outcome]is not a subsystem and there is no product feature called "the summarize outcome node".[outcome]is the tracing target derived from this module's path — the generic node-completion handler every node passes through — andsummarizeis just an id a graph author chose.API Or Behavior Changes
None to any public API.
finish_executionispub(super)and gains one parameter; its only caller isengine/build/activation.rs, updated here.Behaviour is log-only: no control flow is touched. The failure was already routed independently of this line — the
ExecutionStep { status: StepStatus::Error, .. }pushed just below it,observer.on_step_finish, and thecontinue/route/stoppolicy after it are all unchanged.One thing for a reviewer to weigh:
EngineErrorcan carry tool and config text, so this is a new place where payload fragments may reach logs. It is atWARNon a path that only runs after every retry has failed, but it is a real change in what gets written, and every downstream consumer inherits it on the next bump.Tests
New:
tests/node_failure_diagnostics_e2e.rs. It registers a hand-rolled globaltracing::Subscriberthat keeps every event with its fields keyed by name, runs a two-node graph whosetool_callhas noslug(the deterministicCapabilityfailuretests/error_recovery_e2e.rsalready uses) withretry.max_attempts: 3, and asserts the warning carriesnode,error,attempts = 3andon_error = stop.Two deliberate choices:
tracingitself rather than pulling intracing-subscriber; this crate takestracingwithdefault-features = falseand carries no subscriber dep. Registered globally rather than viawith_defaultbecause the engine runs on tokio worker threads and a thread-local dispatcher would miss them — eachtests/*.rsis its own binary, so the one global registration is safe.errormerely renders non-empty would still pass if the field were dropped again, because a missing field and an empty one are indistinguishable once formatted.Proven to fail without the fix. Reverting the warning to its previous form and keeping the test:
fields were {"node": "summarize"}is the whole problem in one line.Commands run locally on this branch, all exit 0:
cargo fmt --checkcargo clippy --all-targets -- -D warningscargo clippy --all-targets --all-features -- -D warningscargo build --all-targetscargo build --all-targets --all-featurescargo test— 1376 passed, 0 failedcargo test --all-features— 1421 passed, 0 failedThe full suite matters here rather than a scoped filter:
finish_execution's signature changed, so every caller and every test that drives a failing node had to be re-checked, not just the new file.Documentation
None needed — the rationale for logging at this site rather than after the unwrap is a comment at the call site, where the next person to move the line will read it.
Summary by CodeRabbit
Bug Fixes
Tests